package net.asrsmid.invoker.as;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;

/**
 * public class Table <br><br>
 * The class Table is a data structure used to save the 
 * multidimensional data queried from Analysis Services.
 * @author Chen WANG
 * @since 19 June 2011
 */
public class Table {
	public static final int MAX_DATA_PRECISION = 5;
	public static final int DEFAULT_DATA_PRECISION = 2;

	public static final int ASIX_ALIAS_COLUMNS = 0;
	public static final int ASIX_ALIAS_ROWS = 1;
	public static final int ASIX_ALIAS_PAGES = 2;
	public static final int ASIX_ALIAS_SECTIONS = 3;
	public static final int ASIX_ALIAS_CHAPTERS = 4;
	
	/*
	 * dimensions : an int array which contains in order dimension length of the 
	 * ... SECTIONS, PAGES, ROWS, COLUMNS. 
	 * Eg. dimensions = [2, 4, 6] means the table dimension is dimensions.length = 3
	 * and the dimension PAGES's length 2, the ROWS's is 4 and the COLUMNS's is 6, which 
	 * is a table of 2 pages, 4 rows and 6 columns
	 */
	private int[] dimensions;
	
	/*
	 * headers : an object array, in which each element is a 
	 * String[][] array (dimension header table)
	 * and its length corresponds to the dimensions' length.
	 * Also in order of ... SECTIONS, PAGES, ROWS, COLUMNS. 
	 */
	private Object[] headers;
	
	/*
	 * data : a multidimensional object array containing the table data.
	 * The dimension of the array equals the field dimensions.
	 * All the table data is in Double if it's numeric, if not then an empty string ""
	 * (in case of null) and an error string "(error)" (in case of exception).  
	 */
	private Object data;

	/* precision : the precision to use when setting the table data */
	private int precision;
	
	/*
	 * coordinates : an int array which contains table's coordinates in its parent table.
	 * Null if table is already the top parent table
	 */
	private int[] coordinates;

	/**
	 * Constructs a Table object with the given fields.
	 * 
	 * @param dimensions an int array which contains in order dimension length of the 
	 * ... SECTIONS, PAGES, ROWS, COLUMNS. 
	 * Eg. dimensions = [2, 4, 6] means the table dimension is dimensions.length = 3
	 * and the dimension PAGES's length 2, the ROWS's is 4 and the COLUMNS's is 6, which 
	 * is a table of 2 pages, 4 rows and 6 columns
	 * 
	 * @param headers an object array, in which each element is a 
	 * String[][] array (dimension header table)
	 * and its length corresponds to the dimensions' length.
	 * Also in order of ... SECTIONS, PAGES, ROWS, COLUMNS.
	 * 
	 * @param data a multidimensional object array containing the table data.
	 * The dimension of the array equals the field dimensions.
	 * All the table data is in Double if it's numeric, if not then an empty string ""
	 * (in case of null) and an error string "(error)" (in case of exception).
	 * 
	 * @throws TableException if all input fields' dimensions don't match or invalid precision
	 */
	public Table(int[] dimensions, Object[] headers, Object data) throws TableException {
		this(dimensions, headers, data, null, DEFAULT_DATA_PRECISION);
	}

	/**
	 * Constructs a Table object with the given fields.
	 * 
	 * @param dimensions an int array which contains in order dimension length of the 
	 * ... SECTIONS, PAGES, ROWS, COLUMNS. 
	 * Eg. dimensions = [2, 4, 6] means the table dimension is dimensions.length = 3
	 * and the dimension PAGES's length 2, the ROWS's is 4 and the COLUMNS's is 6, which 
	 * is a table of 2 pages, 4 rows and 6 columns
	 * 
	 * @param headers an object array, in which each element is a 
	 * String[][] array (dimension header table)
	 * and its length corresponds to the dimensions' length.
	 * Also in order of ... SECTIONS, PAGES, ROWS, COLUMNS.
	 * 
	 * @param data a multidimensional object array containing the table data.
	 * The dimension of the array equals the field dimensions.
	 * All the table data is in Double if it's numeric, if not then an empty string ""
	 * (in case of null) and an error string "(error)" (in case of exception).
	 * 
	 * @param coordinates an int array which contains table's coordinates in its parent table.
	 * Null if table is already the top parent table
	 * 
	 * @throws TableException if all input fields' dimensions don't match or invalid precision
	 */
	public Table(int[] dimensions, Object[] headers, Object data, int[] coordinates) throws TableException {
		this(dimensions, headers, data, coordinates, DEFAULT_DATA_PRECISION);
	}
	
	/**
	 * Constructs a Table object with the given fields.
	 * 
	 * @param dimensions an int array which contains in order dimension length of the 
	 * ... SECTIONS, PAGES, ROWS, COLUMNS. 
	 * Eg. dimensions = [2, 4, 6] means the table dimension is dimensions.length = 3
	 * and the dimension PAGES's length 2, the ROWS's is 4 and the COLUMNS's is 6, which 
	 * is a table of 2 pages, 4 rows and 6 columns
	 * 
	 * @param headers an object array, in which each element is a 
	 * String[][] array (dimension header table)
	 * and its length corresponds to the dimensions' length.
	 * Also in order of ... SECTIONS, PAGES, ROWS, COLUMNS.
	 * 
	 * @param data a multidimensional object array containing the table data.
	 * The dimension of the array equals the field dimensions.
	 * All the table data is in Double if it's numeric, if not then an empty string ""
	 * (in case of null) and an error string "(error)" (in case of exception).
	 * 
	 * @param precision the precision to use when setting the table data
	 * 
	 * @throws TableException if all input fields' dimensions don't match or invalid precision
	 */
	public Table(int[] dimensions, Object[] headers, Object data, int precision) throws TableException {
		this(dimensions, headers, data, null, precision);
	}
	
	/**
	 * Constructs a Table object with the given fields.
	 * 
	 * @param dimensions an int array which contains in order dimension length of the 
	 * ... SECTIONS, PAGES, ROWS, COLUMNS. 
	 * Eg. dimensions = [2, 4, 6] means the table dimension is dimensions.length = 3
	 * and the dimension PAGES's length 2, the ROWS's is 4 and the COLUMNS's is 6, which 
	 * is a table of 2 pages, 4 rows and 6 columns
	 * 
	 * @param headers an object array, in which each element is a 
	 * String[][] array (dimension header table)
	 * and its length corresponds to the dimensions' length.
	 * Also in order of ... SECTIONS, PAGES, ROWS, COLUMNS.
	 * 
	 * @param data a multidimensional object array containing the table data.
	 * The dimension of the array equals the field dimensions.
	 * All the table data is in Double if it's numeric, if not then an empty string ""
	 * (in case of null) and an error string "(error)" (in case of exception).
	 * 
	 * @param coordinates an int array which contains table's coordinates in its parent table.
	 * Null if table is already the top parent table
	 * 
	 * @param precision the precision to use when setting the table data
	 * 
	 * @throws TableException if all input fields' dimensions don't match or invalid precision
	 */
	public Table(int[] dimensions, Object[] headers, Object data, int[] coordinates, int precision) throws TableException {
		// verify input data
		validateTable(dimensions, headers, data, coordinates, precision);
		
		// initialize data
		this.dimensions = dimensions;
		this.data = data;
		this.headers = headers;
		this.precision = precision;
		this.coordinates = coordinates;
	}

	/**
	 * Returns a sub table object at the given coordinates in the table 
	 * @param coords an int array containing the coordinates in of ... PAGES, ROWS  
	 * @return a sub table at the given coordinates in the table
	 * @throws TableException if coords exceeds table's bound
	 */
	public Table getSubTable(int[] coords) throws TableException {
		validateCoordinates(dimensions, coords);

		Table t = null;

		// sub table's dimensions
		int subTabDim = dimensions.length - coords.length;
		int[] subDimensions = new int[subTabDim == 0 ? 1 : subTabDim];
		if (subTabDim == 0) {
			subDimensions[0] = 1;
		} else {
			for (int i = 0; i < subDimensions.length; i++) {
				subDimensions[i] = dimensions[coords.length + i];
			}
		}

		// sub table's data
		Object subData = data;
		for (int i = 0; i < coords.length; i++) {
			subData = Array.get(subData, coords[i]);
		}

		// sub table's headers
		Object[] subHeaders = new Object[subDimensions.length];
		for (int i = 0; i < subHeaders.length; i++) {
			subHeaders[i] = headers[i + coords.length];
		}

		// contstructs new sub table
		t = new Table(subDimensions, subHeaders, subData, coords, precision);
		return t;
	}

	/**
	 * Returns table's dimensions in an array, of which the length indicates the 
	 * table's dimension, and in which each element means a dimension's length.
	 * @return an int array as table's dimensions
	 */
	public int[] getDimensions() {
		return dimensions;
	}

	/**
	 * Returns an object which represents a multidimensional array, the size of which
	 * equals to table's dimensions 
	 * @return an multidimensional array object as table's data
	 */
	public Object getData() {
		return data;
	}

	/**
	 * Returns an int indicating table's data precision 
	 * @return an int as table's data precision
	 */
	public int getPrecision() {
		return precision;
	}

	/**
	 * Returns an object array, in which each element is a string array of 
	 * of header of 2 dimensions for a dimension in the table. 
	 * @return an object array as table's headers
	 */
	public Object[] getHeaders() {
		return headers;
	}
	
	/**
	 * Return a string array of 2 dimensions as the headers for a given dimension.
	 * @param dim the ith dimension, in order of ...PAGES, ROWS, COLUMNS.
	 * @return a string array of 2 dimensions as the headers of the ith dimension.
	 */
	public String[][] getHeaders(int dim) throws TableException {
		if (dim < 0 || dim > dimensions.length)
			throw new DimensionOutOfBoundException(dim, dimensions.length);
		
		return (String[][]) headers[dim];
	}

	/**
	 * Returns an object array as the headers for the given coordinates. 
	 * The length of the input coordinates must be inferior or equal to 
	 * table's dimension. 
	 * Eg. for a table of [2, 4, 6] (2 pages, 4 rows, 6 columns), with 
	 * the coordinates [1, 3], the returned object array contains an string
	 * array (headers[0][i][1]) for the PAGES dimension at position 0, 
	 * and an other string array (headers[1][i][3]) for the ROWS dimension.
	 * @param coords the coordinates at which the headers are requested.
	 * @return an array of string array as the headers at the requested coordinates
	 * @throws TableException if coords exceeds table's bound
	 */
	public Object[] getHeaders(int[] coords) throws TableException {
		// verify coordinates
		validateCoordinates(dimensions, coords);

		// load headers
		int cDim = coords.length;
		Object[] hdrs = new Object[cDim];
		
		for (int i = 0; i < cDim; i++) {
			// headers at the ith dimension
			Object iDimHeaders = headers[i];
			
			// the headers' level
			int hdrsLevel = Array.getLength(iDimHeaders);
			
			// the this coordinate of ith dimension, there are hdrsLevel labels (headers) 
			String[] headers = new String[hdrsLevel];
			hdrs[i] = headers;
			
			// initialize data
			for (int j = 0; j < hdrsLevel; j++) {
				Object subArray = Array.get(iDimHeaders, j);
				headers[j] = (String) Array.get(subArray, coords[i]);
			}
		}
		return hdrs;
	}

	/**
	 * Returns table's coordinates in it's parent table if it has, 
	 * or null if has not a parent table. The int array containing the 
	 * coordinates is in order of PAGES, ROWS, COLUMNS 
	 * @return the table's coordinates in it's parent table 
	 */
	public int[] getCoordinates() {
		return coordinates;
	}
	
	/**
	 * If the coords' length equals the table dimension, returns the numeric value (Double) 
	 * at the given coordinates if there is otherwise an String ("" or "(error)").<br/>
	 * If the coords' length equals the table dimension, returns an array at the indicated 
	 * coordinates.
	 * @param coords an int array containing coordinates in PAGES, ROWS, COLUMNS order
	 * @return an data object at the indicated coordinates.
	 * @throws TableException if coords exceeds table's bound
	 */
	public Object getData(int[] coords) throws TableException {
		/* verify input coordinates */
		validateCoordinates(dimensions, coords);

		int[] c = new int[dimensions.length];
		for (int i = 0; i < c.length; i++) {
			if (i > coords.length - 1)
				c[i] = 0;
			else
				c[i] = coords[i];
		}

		Object d = this.data;
		for (int i = 0; i < c.length; i++) {
			d = Array.get(d, c[i]);
		}
		return d;
	}

	private void validateTable(int[] dimensions, Object[] headers, Object data, int[] coordinates, int precision) throws TableException {
		/* table dimension */
		int tDim = dimensions.length;
		
		/* headers dimension */
		int hDim = headers.length;
		
		/*
		 * hDims is an array of 2 dims and each element is an array of 2 --> which means a header table.
		 * The first element of this  array of 2 is it's first level'length of the header table.
		 * Eg. for axis(0) : [2, 4] means the column header table has 2 rows and 4 columns and 4 equals 
		 * data table's columns number and 2 indicates the column header's depth is 2 
		 */
		int[][] hDims = new int[hDim][2];
		for (int i = 0; i < hDim; i++) {
			hDims[i][0] = Array.getLength(headers[i]);
			hDims[i][1] = Array.getLength(Array.get(headers[i], 0));
		}
		
		/* data dimension */
		Object d = data;
		List<Integer> dDims = new ArrayList<Integer>();
		while (d instanceof Object[]) {
			dDims.add(Array.getLength(d));
			d = Array.get(d, 0);
		}
		int dDim = dDims.size();
		
		/*
		 * if input dimensions' length != headers' length != data's depth --> DimensionsNotMatchException
		 * else check if each dimension of the input has the same size 
		 */
		if (tDim != hDim || tDim != dDim || hDim != dDim) {
			throw new DimensionsNotMatchException(tDim, hDim, dDim);
		} else {
			// dim length (table and data), (data and headers)
			for (int i = 0; i < tDim; i++) {
				/*
				 * if the ith dimension's length != data's ith layer's length --> DimensionsNotMatchException
				 * else the ith dimension's length != ith header's length --> DimensionsNotMatchException
				 */
				if (dimensions[i] != dDims.get(i)) {
					throw new DimensionsNotMatchException(dimensions, dDims);
				} else if (dimensions[i] != hDims[i][1]) {
					throw new DimensionsNotMatchException(dimensions, hDims);
				}
			}
		}
		
		/*
		 * if input precision is not between 0 and MAX_DATA_PRECISION --> InvalidDataPrecisionException
		 */
		if (precision < 0 || precision > MAX_DATA_PRECISION)
			throw new InvalidDataPrecisionException(precision, MAX_DATA_PRECISION);
		
		/*
		 * if input coordinates != null --> it's a sub table of the parent table
		 */
		if (coordinates != null) {
			int cDim = coordinates.length;
			/*
			 * if input coordinates' length > dimensions' length --> DimensionOutOfBoundException
			 * else coordinates[i] >= dimensions[i] --> CoordinatesOutOfBoundException
			 */
			if (cDim > tDim) {
				throw new DimensionOutOfBoundException(cDim, tDim);
			} else {
				for (int i = 0; i < cDim; i++) {
					/* a coordinate[i] interval == [0, dimensions[i] -1] */
					if (coordinates[i] < 0 && coordinates[i] >= dimensions[i])
						throw new CoordinatesOutOfBoundException(coordinates, dimensions);
				}
			}
		}
	}

	private void validateCoordinates(int[] dimensions, int[] coords) throws TableException {
		int tDim = dimensions.length;
		int cDim = coords.length;
		
		/*
		 * if the input coords' length > dimensions' length --> DimensionOutOfBoundException
		 * else check if coords[i] is between 0 and dimensions[i] - 1
		 */
		if (cDim > tDim) {
			throw new DimensionOutOfBoundException(cDim, tDim);
		} else {
			for (int i = 0; i < cDim; i++) {
				/*
				 * if coords[i] is not [0, dimensions[i] - 1] --> CoordinatesOutOfBoundException
				 */
				if (coords[i] < 0 && coords[i] >= dimensions[i])
					throw new CoordinatesOutOfBoundException(coords, dimensions);
			}
		}
	}
}